Skip to content

feat: improve flag enricher#1786

Merged
k11kirky merged 1 commit intomainfrom
04-21-feat_improve_flag_enricher
Apr 23, 2026
Merged

feat: improve flag enricher#1786
k11kirky merged 1 commit intomainfrom
04-21-feat_improve_flag_enricher

Conversation

@k11kirky
Copy link
Copy Markdown
Contributor

@k11kirky k11kirky commented Apr 21, 2026

TLDR

Flag comments now include active/inactive status, 7-day evaluation stats (evals and unique users), and a direct link to the flag in PostHog.

Problem

Flag annotations in code comments lacked context about whether a flag was actually active, how much traffic it was seeing, and where to find it in PostHog. Developers had to navigate to PostHog manually to get this information.

Changes

  • Added getFlagEvaluationStats to PostHogApi, which queries HogQL for $feature_flag_called events over a configurable lookback window (defaulting to 7 days) and returns per-flag evaluation counts and unique user counts
  • Added buildFlagUrl helper that constructs a direct PostHog URL to a feature flag given the host, project ID, and flag ID
  • Flag comments now include:
    • active / inactive status derived from the flag's active field
    • Evaluation stats formatted as N evals / M users (7d) when available
    • A direct URL to the flag in PostHog
    • not in PostHog when the flag key has no matching flag
  • Flags not found in PostHog now short-circuit the comment formatter immediately rather than producing a partial comment
  • EnrichedFlag, EnrichedListItem, and EnrichmentContext types extended to carry url, evaluationStats, active, host, and projectId
  • ParseResult.enrichFromApi now fetches flag evaluation stats in parallel with other API calls and passes host and projectId through the enrichment context

Copy link
Copy Markdown
Contributor Author

k11kirky commented Apr 21, 2026

This stack of pull requests is managed by Graphite. Learn more about stacking.

@k11kirky k11kirky marked this pull request as ready for review April 21, 2026 20:36
@greptile-apps
Copy link
Copy Markdown
Contributor

greptile-apps Bot commented Apr 21, 2026

Prompt To Fix All With AI
This is a comment left during a code review.
Path: packages/enricher/src/enricher.test.ts
Line: 402-499

Comment:
**Prefer parameterised tests**

The five new `toComments` tests share the same three-step skeleton — parse a snippet, call `mockApiResponses`, assert on `toComments()` output — and differ only in the flag configuration and expected substrings. Per the team's convention they should be a single `test.each` table, e.g.:

```ts
test.each([
  {
    name: "rich line with url, active, rollout, evals",
    flagOpts: { id: 42, filters: { groups: [{ rollout_percentage: 60, properties: [] }] } },
    flagKey: "my-flag",
    evalStats: [["my-flag", 1240, 230]] as [string, number, number][],
    contains: ["active", "60% rolled out", "1,240 evals / 230 users (7d)", "feature_flags/42"],
    notContains: [],
  },
  // … inactive, ghost, quiet-flag variants …
])("toComments $name", async ({ flagKey, flagOpts, evalStats, contains, notContains }) => {
  const result = await enricher.parse(`posthog.getFeatureFlag('${flagKey}');`, "javascript");
  mockApiResponses({ flags: [makeFlag(flagKey, flagOpts)], flagEvalStats: evalStats });
  const annotated = (await result.enrichFromApi(API_CONFIG)).toComments();
  contains.forEach((s) => expect(annotated).toContain(s));
  notContains.forEach((s) => expect(annotated).not.toContain(s));
});
```

This reduces repetition without losing coverage.

**Context Used:** Do not attempt to comment on incorrect alphabetica... ([source](https://app.greptile.com/review/custom-context?memory=instruction-0))

How can I resolve this? If you propose a fix, please make it concise.

---

This is a comment left during a code review.
Path: packages/enricher/src/comment-formatter.ts
Line: 20-22

Comment:
**"inactive" appears twice for the same flag**

When a flag has `active: false`, the comment will contain both the explicit `"inactive"` segment (line 21) and `"STALE (inactive)"` from the staleness classifier — confirmed by the test that asserts both are present. The staleness segment already communicates the reason the flag is inactive, so displaying `"inactive"` as a separate part is redundant and violates the OnceAndOnlyOnce simplicity rule.

Consider either suppressing the `active`/`inactive` label when `flag.staleness === "inactive"`, or only showing the active/inactive label and dropping it from the staleness display.

**Context Used:** Do not attempt to comment on incorrect alphabetica... ([source](https://app.greptile.com/review/custom-context?memory=instruction-0))

How can I resolve this? If you propose a fix, please make it concise.

Reviews (1): Last reviewed commit: "feat: improve flag enricher" | Re-trigger Greptile

Comment on lines +402 to 499
flagEvalStats: [["my-flag", 1240, 230]],
});
const enriched = await result.enrichFromApi(API_CONFIG);

expect(enriched.flags[0].url).toBe(
"https://test.posthog.com/project/1/feature_flags/42",
);
expect(enriched.flags[0].evaluationStats).toEqual({
evaluations: 1240,
uniqueUsers: 230,
});
});

test("toComments renders rich flag line with url, active, rollout, evals", async () => {
const code = `posthog.getFeatureFlag('my-flag');`;
const result = await enricher.parse(code, "javascript");

mockApiResponses({
flags: [
makeFlag("my-flag", {
id: 42,
filters: { groups: [{ rollout_percentage: 60, properties: [] }] },
}),
],
flagEvalStats: [["my-flag", 1240, 230]],
});
const enriched = await result.enrichFromApi(API_CONFIG);

const annotated = enriched.toComments();
expect(annotated).toContain(`Flag: "my-flag"`);
expect(annotated).toContain("active");
expect(annotated).toContain("60% rolled out");
expect(annotated).toContain("1,240 evals / 230 users (7d)");
expect(annotated).toContain(
"https://test.posthog.com/project/1/feature_flags/42",
);
});

test("toComments marks inactive flags explicitly", async () => {
const code = `posthog.getFeatureFlag('off-flag');`;
const result = await enricher.parse(code, "javascript");

mockApiResponses({ flags: [makeFlag("off-flag", { active: false })] });
const enriched = await result.enrichFromApi(API_CONFIG);

const annotated = enriched.toComments();
expect(annotated).toContain("inactive");
expect(annotated).toContain("STALE (inactive)");
});

test("toComments handles flag not in PostHog", async () => {
const code = `posthog.getFeatureFlag('ghost-flag');`;
const result = await enricher.parse(code, "javascript");

mockApiResponses({ flags: [] });
const enriched = await result.enrichFromApi(API_CONFIG);

const annotated = enriched.toComments();
expect(annotated).toContain(`Flag: "ghost-flag" \u2014 not in PostHog`);
});

test("toComments omits evaluation segment when stats missing", async () => {
const code = `posthog.getFeatureFlag('quiet-flag');`;
const result = await enricher.parse(code, "javascript");

mockApiResponses({ flags: [makeFlag("quiet-flag", { id: 7 })] });
const enriched = await result.enrichFromApi(API_CONFIG);

const annotated = enriched.toComments();
expect(annotated).toContain(`Flag: "quiet-flag"`);
expect(annotated).not.toContain("evals /");
expect(annotated).toContain(
"https://test.posthog.com/project/1/feature_flags/7",
);
});

test("getFlagEvaluationStats is called with detected flag keys", async () => {
const code = `posthog.getFeatureFlag('tracked-flag');`;
const result = await enricher.parse(code, "javascript");

mockApiResponses({
flags: [makeFlag("tracked-flag")],
flagEvalStats: [["tracked-flag", 10, 5]],
});
await result.enrichFromApi(API_CONFIG);

const calls = vi.mocked(fetch).mock.calls;
const queryPost = calls.find(
([url, init]) =>
String(url).includes("/query/") && init?.method === "POST",
);
expect(queryPost).toBeDefined();
const body = String(queryPost?.[1]?.body ?? "");
expect(body).toContain("$feature_flag_called");
expect(body).toContain("tracked-flag");
});

test("enrichFromApi with no detected usage returns empty enrichment", async () => {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Prefer parameterised tests

The five new toComments tests share the same three-step skeleton — parse a snippet, call mockApiResponses, assert on toComments() output — and differ only in the flag configuration and expected substrings. Per the team's convention they should be a single test.each table, e.g.:

test.each([
  {
    name: "rich line with url, active, rollout, evals",
    flagOpts: { id: 42, filters: { groups: [{ rollout_percentage: 60, properties: [] }] } },
    flagKey: "my-flag",
    evalStats: [["my-flag", 1240, 230]] as [string, number, number][],
    contains: ["active", "60% rolled out", "1,240 evals / 230 users (7d)", "feature_flags/42"],
    notContains: [],
  },
  // … inactive, ghost, quiet-flag variants …
])("toComments $name", async ({ flagKey, flagOpts, evalStats, contains, notContains }) => {
  const result = await enricher.parse(`posthog.getFeatureFlag('${flagKey}');`, "javascript");
  mockApiResponses({ flags: [makeFlag(flagKey, flagOpts)], flagEvalStats: evalStats });
  const annotated = (await result.enrichFromApi(API_CONFIG)).toComments();
  contains.forEach((s) => expect(annotated).toContain(s));
  notContains.forEach((s) => expect(annotated).not.toContain(s));
});

This reduces repetition without losing coverage.

Context Used: Do not attempt to comment on incorrect alphabetica... (source)

Prompt To Fix With AI
This is a comment left during a code review.
Path: packages/enricher/src/enricher.test.ts
Line: 402-499

Comment:
**Prefer parameterised tests**

The five new `toComments` tests share the same three-step skeleton — parse a snippet, call `mockApiResponses`, assert on `toComments()` output — and differ only in the flag configuration and expected substrings. Per the team's convention they should be a single `test.each` table, e.g.:

```ts
test.each([
  {
    name: "rich line with url, active, rollout, evals",
    flagOpts: { id: 42, filters: { groups: [{ rollout_percentage: 60, properties: [] }] } },
    flagKey: "my-flag",
    evalStats: [["my-flag", 1240, 230]] as [string, number, number][],
    contains: ["active", "60% rolled out", "1,240 evals / 230 users (7d)", "feature_flags/42"],
    notContains: [],
  },
  // … inactive, ghost, quiet-flag variants …
])("toComments $name", async ({ flagKey, flagOpts, evalStats, contains, notContains }) => {
  const result = await enricher.parse(`posthog.getFeatureFlag('${flagKey}');`, "javascript");
  mockApiResponses({ flags: [makeFlag(flagKey, flagOpts)], flagEvalStats: evalStats });
  const annotated = (await result.enrichFromApi(API_CONFIG)).toComments();
  contains.forEach((s) => expect(annotated).toContain(s));
  notContains.forEach((s) => expect(annotated).not.toContain(s));
});
```

This reduces repetition without losing coverage.

**Context Used:** Do not attempt to comment on incorrect alphabetica... ([source](https://app.greptile.com/review/custom-context?memory=instruction-0))

How can I resolve this? If you propose a fix, please make it concise.

Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!

Comment on lines +20 to +22
if (flag.rollout !== null) {
parts.push(`${flag.rollout}% rolled out`);
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 "inactive" appears twice for the same flag

When a flag has active: false, the comment will contain both the explicit "inactive" segment (line 21) and "STALE (inactive)" from the staleness classifier — confirmed by the test that asserts both are present. The staleness segment already communicates the reason the flag is inactive, so displaying "inactive" as a separate part is redundant and violates the OnceAndOnlyOnce simplicity rule.

Consider either suppressing the active/inactive label when flag.staleness === "inactive", or only showing the active/inactive label and dropping it from the staleness display.

Context Used: Do not attempt to comment on incorrect alphabetica... (source)

Prompt To Fix With AI
This is a comment left during a code review.
Path: packages/enricher/src/comment-formatter.ts
Line: 20-22

Comment:
**"inactive" appears twice for the same flag**

When a flag has `active: false`, the comment will contain both the explicit `"inactive"` segment (line 21) and `"STALE (inactive)"` from the staleness classifier — confirmed by the test that asserts both are present. The staleness segment already communicates the reason the flag is inactive, so displaying `"inactive"` as a separate part is redundant and violates the OnceAndOnlyOnce simplicity rule.

Consider either suppressing the `active`/`inactive` label when `flag.staleness === "inactive"`, or only showing the active/inactive label and dropping it from the staleness display.

**Context Used:** Do not attempt to comment on incorrect alphabetica... ([source](https://app.greptile.com/review/custom-context?memory=instruction-0))

How can I resolve this? If you propose a fix, please make it concise.

Copy link
Copy Markdown
Contributor Author

k11kirky commented Apr 23, 2026

Merge activity

  • Apr 23, 10:03 AM UTC: A user started a stack merge that includes this pull request via Graphite.
  • Apr 23, 10:03 AM UTC: @k11kirky merged this pull request with Graphite.

@k11kirky k11kirky merged commit 0fe96d7 into main Apr 23, 2026
16 checks passed
@k11kirky k11kirky deleted the 04-21-feat_improve_flag_enricher branch April 23, 2026 10:03
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants